<html>
<head>
  <meta http-equiv="content-type" content="text/html; charset=utf-8" lang="en"></meta>
  <title>OSM3S on Mapnik via Open Layers</title>
  <script src="https://openlayers.org/api/2.11/OpenLayers.js"></script>
  <script src="https://openstreetmap.org/openlayers/OpenStreetMap.js"></script>
  <script src="https://overpass-api.de/overpass.js"></script>
  <script type="text/javascript">
      var lat = "$LAT";
      var lon = "$LON";
      var zoom = "$ZOOM";
      var current_id = "$ID";
      var data_url = "https://overpass-api.de/api/augmented_diff?id=";
      var map;

/**
 * Class: AugmentedDiffsFormat
 *
 * Inherits from:
 *  - <OpenLayers.Format.XML>
 */
AugmentedDiffsFormat = OpenLayers.Class(OpenLayers.Format.XML, {

    /**
     * Constructor: AugmentedDiffsFormat
     * Create a new parser for OSM.
     *
     * Parameters:
     * options - {Object} An optional object whose properties will be set on
     *     this instance.
     */
    initialize: function(options) {

        var layer_defaults = {};
        layer_defaults = OpenLayers.Util.extend(layer_defaults, options);

        // OSM coordinates are always in longlat WGS84
        this.externalProjection = new OpenLayers.Projection("EPSG:4326");

        OpenLayers.Format.XML.prototype.initialize.apply(this, [layer_defaults]);
    },

    /**
     * APIMethod: read
     * Return a list of features from a OSM doc
     *
     * Parameters:
     * doc - {Element}
     *
     * Returns:
     * Array({<OpenLayers.Feature.Vector>})
     */
    read: function(doc) {

        if (typeof doc == "string") {
            doc = OpenLayers.Format.XML.prototype.read.apply(this, [doc]);
        }

        var erasedNodes = {};
        var keptNodes = {};
        var insertedNodes = {};
        this.parseNodes(doc, erasedNodes, keptNodes, insertedNodes);
        var erasedWays = {};
        var keptWays = {};
        var insertedWays = {};
        this.parseWays(doc, erasedWays, keptWays, insertedWays);

        for (var node in keptNodes)
        {
            erasedNodes[node] = keptNodes[node];
            insertedNodes[node] = keptNodes[node];
        }

        for (var way_id in keptWays)
        {
            erasedWays[way_id] = keptWays[way_id];
            insertedWays[way_id] = keptWays[way_id];
        }

        var deleteStyle = {
              strokeColor: "red",
              strokeOpacity: 0.5,
              strokeWidth: 6,
              pointRadius: 10,
              fillColor: "red",
              fillOpacity: 0.25
          };

        var insertStyle = {
              strokeColor: "green",
              strokeOpacity: 0.5,
              strokeWidth: 6,
              pointRadius: 10,
              fillColor: "green",
              fillOpacity: 0.25
          };

        var tagChangedStyle = {
              strokeColor: "yellow",
              strokeOpacity: 0.5,
              strokeWidth: 6,
              pointRadius: 10,
              fillColor: "yellow",
              fillOpacity: 0.25
          };

        var unchangedStyle = {
              strokeColor: "grey",
              strokeOpacity: 0.5,
              strokeWidth: 6,
              pointRadius: 10,
              fillColor: "grey",
              fillOpacity: 0.25
          };

        // Geoms will contain at least ways.length entries.
        var feat_list = [];

        for (var way_id in erasedWays) {
            var feat = this.makeWayFeature(erasedWays[way_id], erasedNodes);
            if (insertedWays[way_id] &&
                    this.equalGeometry(erasedWays[way_id], erasedNodes, insertedWays[way_id], insertedNodes)) {
                if (insertedWays[way_id].tagsEqual)
                    feat.style = unchangedStyle;
                else
                {
                    this.enlargeBoundsWay(erasedWays[way_id], erasedNodes);
                    feat.style = tagChangedStyle;
                }
            }
            else
            {
                this.enlargeBoundsWay(erasedWays[way_id], erasedNodes);
                feat.style = deleteStyle;
            }
            feat_list.push(feat);
        }
        for (var way_id in insertedWays) {
            if (erasedWays[way_id] &&
                    this.equalGeometry(erasedWays[way_id], erasedNodes, insertedWays[way_id], insertedNodes))
                continue;
            var feat = this.makeWayFeature(insertedWays[way_id], insertedNodes);
            this.enlargeBoundsWay(insertedWays[way_id], insertedNodes);
            feat.style = insertStyle;
            feat_list.push(feat);
        }

        for (var node_id in erasedNodes) {
            var node = erasedNodes[node_id];
            if (!node.used) {

                var feat = new OpenLayers.Feature.Vector(
                    new OpenLayers.Geometry.Point(node['lon'], node['lat']), null);
                if (this.internalProjection && this.externalProjection) {
                    feat.geometry.transform(this.externalProjection,
                        this.internalProjection);
                }
                feat.osm_id = parseInt(node_id);

                if (insertedNodes[node_id] &&
                        insertedNodes[node_id].lon == node.lon &&
                        insertedNodes[node_id].lat == node.lat) {
                    if (insertedNodes[node_id].tagsEqual)
                        feat.style = unchangedStyle;
                    else
                    {
                        feat.style = tagChangedStyle;
                        this.enlargeBounds(node['lat'], node['lon']);
                    }
                    insertedNodes[node_id].used = true;
                }
                else {
                    this.enlargeBounds(node['lat'], node['lon']);
                    feat.style = deleteStyle;
                }
                feat_list.push(feat);
            }
        }
        for (var node_id in insertedNodes) {
            var node = insertedNodes[node_id];
            if (!node.used) {

                this.enlargeBounds(node['lat'], node['lon']);
                var feat = new OpenLayers.Feature.Vector(
                    new OpenLayers.Geometry.Point(node['lon'], node['lat']), null);
                if (this.internalProjection && this.externalProjection) {
                    feat.geometry.transform(this.externalProjection,
                        this.internalProjection);
                }
                feat.osm_id = parseInt(node_id);
                feat.style = insertStyle;
                feat_list.push(feat);
            }
        }

        if (this.minLat < 90.0)
        {
            var lonLat = new OpenLayers.LonLat(
                (Number(this.minLon) + Number(this.maxLon))/2.0,
                (Number(this.minLat) + Number(this.maxLat))/2.0)
                .transform(new OpenLayers.Projection("EPSG:4326"), new OpenLayers.Projection("EPSG:900913"));

            var southwest = new OpenLayers.LonLat(
                Number(this.minLon), Number(this.minLat))
                .transform(new OpenLayers.Projection("EPSG:4326"), new OpenLayers.Projection("EPSG:900913"));

            var northeast = new OpenLayers.LonLat(
                Number(this.maxLon), Number(this.maxLat))
                .transform(new OpenLayers.Projection("EPSG:4326"), new OpenLayers.Projection("EPSG:900913"));

            if (map.getExtent().containsLonLat(southwest) && map.getExtent().containsLonLat(northeast))
            {
                map.zoomTo(18);
                map.setCenter(lonLat);

                var extent = map.getExtent();
                extent.extend(southwest);
                extent.extend(northeast);
                map.zoomToExtent(extent);
            }
        }

        return feat_list;
    },


    parseNodes: function(doc, erasedNodes, keptNodes, insertedNodes) {
        var node_list = doc.getElementsByTagName("node");
        var lastTags;
        var lastErasedId;
        for (var i = 0; i < node_list.length; i++) {
            var node = node_list[i];
            var id = node.getAttribute("id");
            var nodeObj = {
                'lat': node.getAttribute("lat"),
                'lon': node.getAttribute("lon"),
                'tagsEqual': true
            };
            if (node.parentNode.tagName == "old") {
                erasedNodes[id] = nodeObj;
                lastTags = [];
                lastErasedId = id;
                var tags = node.getElementsByTagName("tag");
                for (var j = 0; j < tags.length; j++)
                    lastTags[j] = {
                        'k': tags[j].getAttribute("k"),
                        'v': tags[j].getAttribute("v")
                        };

            } else if (node.parentNode.tagName == "action" &&
                    node.parentNode.getAttribute("type") == "info") {
                keptNodes[id] = nodeObj;

            } else if ((node.parentNode.tagName == "new" &&
                    node.parentNode.parentNode.getAttribute("type") != "delete") ||
                    (node.parentNode.tagName == "action" &&
                    node.parentNode.getAttribute("type") == "create")) {
                insertedNodes[id] = nodeObj;
                var tags = node.getElementsByTagName("tag");
                if (lastErasedId != id || tags.length != lastTags.length)
                    nodeObj['tagsEqual'] = false;
                else {
                    for (var j = 0; j < tags.length; j++) {
                        if (lastTags[j]['k'] != tags[j].getAttribute("k") ||
                                lastTags[j]['v'] != tags[j].getAttribute("v")) {
                            nodeObj['tagsEqual'] = false;
                            break;
                        }
                    }
                }
            }
        }
    },


    minLat: 90.0,
    maxLat: -90.0,
    minLon: 180.0,
    maxLon: -180.0,

    enlargeBounds: function(lat, lon) {

        if (lat < this.minLat)
            this.minLat = lat;
        if (lat > this.maxLat)
            this.maxLat = lat;
        if (lon < this.minLon)
            this.minLon = lon;
        if (lon > this.maxLon)
            this.maxLon = lon;
    },


    enlargeBoundsWay: function(way, nodes) {

        for (var j = 0; j < way.nodes.length; j++) {
            var node = nodes[way.nodes[j]];

            if (!node)
              continue;

            this.enlargeBounds(node['lat'], node['lon']);
        }
    },

    /**
     * Method: getWays
     * Return the way items from a doc.
     *
     * Parameters:
     * doc - {DOMElement} node to parse tags from
     */
    parseWays: function(doc, erasedWays, keptWays, insertedWays) {
        var way_list = doc.getElementsByTagName("way");
        var lastTags;
        var lastErasedId;
        for (var i = 0; i < way_list.length; i++) {
            var way = way_list[i];
            var id = way.getAttribute("id");
            var wayObj = {
                'id': id,
                'tagsEqual': true
            };

            var node_list = way.getElementsByTagName("nd");

            wayObj.nodes = new Array(node_list.length);

            for (var j = 0; j < node_list.length; j++) {
                wayObj.nodes[j] = node_list[j].getAttribute("ref");
            }
            if (way.parentNode.tagName == "old") {
                erasedWays[wayObj.id] = wayObj;
                lastTags = [];
                lastErasedId = id;
                var tags = way.getElementsByTagName("tag");
                for (var j = 0; j < tags.length; j++)
                    lastTags[j] = {
                        'k': tags[j].getAttribute("k"),
                        'v': tags[j].getAttribute("v")
                        };

            } else if (way.parentNode.tagName == "action" &&
                    way.parentNode.getAttribute("type") == "info") {
                keptWays[id] = wayObj;

            } else if ((way.parentNode.tagName == "new" &&
                    way.parentNode.parentNode.getAttribute("type") != "delete") ||
                    (way.parentNode.tagName == "action" &&
                    way.parentNode.getAttribute("type") == "create")) {
                insertedWays[wayObj.id] = wayObj;
                var tags = way.getElementsByTagName("tag");
                if (lastErasedId != id || tags.length != lastTags.length)
                    wayObj['tagsEqual'] = false;
                else {
                    for (var j = 0; j < tags.length; j++) {
                        if (lastTags[j]['k'] != tags[j].getAttribute("k") ||
                                lastTags[j]['v'] != tags[j].getAttribute("v")) {
                            wayObj['tagsEqual'] = false;
                            break;
                        }
                    }
                }
            }
        }
    },

    makeWayFeature: function(way, nodes) {
        var point_list = new Array(way.nodes.length);

        var missingNodes = "";
        for (var j = 0; j < way.nodes.length; j++) {
            var node = nodes[way.nodes[j]];

            if (!node) {
              missingNodes += way.nodes[j] + "\n";
              continue;
            }

            var point = new OpenLayers.Geometry.Point(node.lon, node.lat);

            // Since OSM is topological, we stash the node ID internally.
            point.osm_id = parseInt(way.nodes[j]);
            point_list[j] = point;

            // We don't display nodes if they're used inside other
            // elements.
            node.used = true;
        }
        if (missingNodes != "")
          alert("Way " + way.id + " lacks nodes\n" + missingNodes);

        var geometry = new OpenLayers.Geometry.LineString(point_list);
        if (this.internalProjection && this.externalProjection) {
            geometry.transform(this.externalProjection,
                this.internalProjection);
        }

        var feat = new OpenLayers.Feature.Vector(geometry, way.tags);
        feat.osm_id = parseInt(way.id);

        return feat;
    },

    equalGeometry: function(way1, nodes1, way2, nodes2) {

        if (way1.nodes.length != way2.nodes.length)
            return false;

        for (var j = 0; j < way1.nodes.length; j++) {
            var node1 = nodes1[way1.nodes[j]];
            var node2 = nodes2[way2.nodes[j]];
            if (!node1 || !node2)
                return false;

            if (node1.lon != node2.lon || node1.lat != node2.lat)
                return false;
        }

        return true;
    },

    /**
     * APIMethod: write
     */
    write: function(features) {
        return "";
    },

    CLASS_NAME: "AugmentedDiffsFormat"
});

      function add_layer(id) {

          var styleMap = new OpenLayers.StyleMap({
              strokeColor: "blue",
              strokeOpacity: 0.5,
              strokeWidth: 6,
              pointRadius: 10,
              fillColor: "blue",
              fillOpacity: 0.25
          });

          var layer = new OpenLayers.Layer.Vector("Diff " + id, {
              strategies: [new OpenLayers.Strategy.BBOX()],
              protocol: new OpenLayers.Protocol.HTTP({
                  url: data_url + id,
                  format: new AugmentedDiffsFormat()
              }),
              styleMap: styleMap,
              projection: new OpenLayers.Projection("EPSG:4326"),
              ratio: 1.0
          });

          map.addLayers([layer]);
      }


      function init(){
          map = new OpenLayers.Map ("map", {
          controls:[
              new OpenLayers.Control.Navigation(),
              new OpenLayers.Control.PanZoomBar(),
              new OpenLayers.Control.LayerSwitcher(),
              new OpenLayers.Control.Attribution()],
              maxExtent: new OpenLayers.Bounds(-20037508.34,-20037508.34,20037508.34,20037508.34),
              maxResolution: 156543.0399,
              numZoomLevels: 19,
              units: 'm',
              projection: new OpenLayers.Projection("EPSG:900913"),
              displayProjection: new OpenLayers.Projection("EPSG:4326")
          } );

          layerMapnik = new OpenLayers.Layer.OSM.Mapnik("Mapnik");
          map.addLayer(layerMapnik);
          layerCycleMap = new OpenLayers.Layer.OSM.CycleMap("CycleMap");
          map.addLayer(layerCycleMap);

          var lonLat = new OpenLayers.LonLat(lon, lat).transform(new OpenLayers.Projection("EPSG:4326"), new OpenLayers.Projection("EPSG:900913"));

          map.setCenter (lonLat, zoom);

          //Initialise the vector layer using AugmentedDiffsFormat
          add_layer(current_id);
      }
  </script>
</head>
<body onload="init()">
  <div id="map" class="smallmap"></div>
  <button onclick="current_id++;add_layer(current_id)">Add diff</button>
</body>
</html>
